namespace taurus.client {

    /**
     * the data type.
     */
    export const enum TDataType {
        NULL,
        BOOL,
        BYTE,
        SHORT,
        INT,
        LONG,
        FLOAT,
        DOUBLE,
        STRING,
        BYTE_ARRAY,
        TARRAY,
        TOBJECT
    }

    /**
     * TDataWrapper.
     */
    export class TDataWrapper {
        public data: any;
        public type: number;


        public constructor(tp: TDataType, data: any) {
            this.type = tp;
            this.data = data;
        }
    }

    /**
     * Data serializer.
     */
    export class TDataSerializer {
        private static instance: TDataSerializer;

        public static me(): TDataSerializer {
            if (TDataSerializer.instance == null) {
                TDataSerializer.instance = new TDataSerializer();
            }
            return TDataSerializer.instance;
        }

        array2binary(array: ITArray): ByteArray {
            var buffer = new ByteArray();
            buffer.writeByte(TDataType.TARRAY);
            buffer.writeShort(array.size());
            return this.arr2bin(array, buffer);
        }

        object2binary(obj: ITObject): ByteArray {
            var buffer = new ByteArray();
            buffer.writeByte(TDataType.TOBJECT);
            buffer.writeShort(obj.size());
            return this.obj2bin(obj, buffer);
        }

        binary2array(data: ByteArray): ITArray {
            if (data.length < 3) {
                throw Error("Can't decode an Array. Byte data is insufficient. Size: " + data.length + " byte(s)");
            }
            data.position = 0;
            return this.decodeTArray(data);
        }

        binary2object(data: ByteArray): ITObject {
            if (data.length < 3) {
                throw "Can't decode an Object. Byte data is insufficient. Size: " + data.length + " byte(s)";
            }
            data.position = 0;
            return this.decodeTObject(data);
        }

        json2object(jsonStr:string):ITObject {
            if (jsonStr.length < 2) {
                throw "Can't decode Object. JSON String is too short. Len: " + jsonStr.length;
            }
            var o = JSON.parse(jsonStr);
            return this.decodeTObject_json(o);
        }
    
        /**
         * 
         * @param jsonStr
         * @return
         */
        json2array(jsonStr:string):ITArray {
            if (jsonStr.length < 2) {
                throw "Can't decode Object. JSON String is too short. Len: " + jsonStr.length;
            }
            var jsa = JSON.parse(jsonStr);
            return this.decodeTArray_json(jsa);
        }

        object2json(obj:ITObject) :string{
            let map:any = {};
            this.flattenObject(map,obj);
            return JSON.stringify(map);
        }
    
        array2json(array:ITArray):string {
            let list = [];
            this.flattenArray(list,array);
            return JSON.stringify(array);
        }


        private addData(buffer: ByteArray, newData: ByteArray): ByteArray {
            buffer.writeBytes(newData.bytes);
            return buffer;
        }

        private arr2bin(array: ITArray, buffer: ByteArray): ByteArray {
            for (var i = 0; i < array.size(); i++) {
                var wrapped = array.get(i);
                buffer = this.encodeObject(buffer, wrapped.type, wrapped.data);
            }
            return buffer;
        }

        private binDecode_BOOL(buffer: ByteArray): TDataWrapper {
            return new TDataWrapper(TDataType.BOOL, buffer.readBoolean());
        }

        private binDecode_BYTE(buffer: ByteArray): TDataWrapper {
            return new TDataWrapper(TDataType.BYTE, buffer.readByte());
        }

        private binDecode_BYTE_ARRAY(buffer: ByteArray): TDataWrapper {
            var count = buffer.readInt();
            if (count < 0) {
                throw "Array negative size: " + count;
            }
            return new TDataWrapper(TDataType.BYTE_ARRAY, buffer.readBytes(count));
        }

        private binDecode_DOUBLE(buffer: ByteArray): TDataWrapper {
            return new TDataWrapper(TDataType.DOUBLE, buffer.readDouble());
        }

        private binDecode_FLOAT(buffer: ByteArray): TDataWrapper {
            return new TDataWrapper(TDataType.FLOAT, buffer.readFloat());
        }

        private binDecode_INT(buffer: ByteArray): TDataWrapper {
            return new TDataWrapper(TDataType.INT, buffer.readInt());
        }

        private binDecode_LONG(buffer: ByteArray): TDataWrapper {
            return new TDataWrapper(TDataType.LONG, buffer.readLong());
        }

        private binDecode_NULL(buffer: ByteArray): TDataWrapper {
            return new TDataWrapper(TDataType.NULL, null);
        }

        private binDecode_SHORT(buffer: ByteArray): TDataWrapper {
            return new TDataWrapper(TDataType.SHORT, buffer.readShort());
        }

        private binDecode_UTF_STRING(buffer: ByteArray): TDataWrapper {
            return new TDataWrapper(TDataType.STRING, buffer.readString());
        }

        private binEncode_BOOL(buffer: ByteArray, val: boolean): ByteArray {
            var newData = new ByteArray();
            newData.writeByte(TDataType.BOOL);
            newData.writeBoolean(val);
            return this.addData(buffer, newData);
        }

        private binEncode_BYTE_ARRAY(buffer: ByteArray, val: Uint8Array): ByteArray {
            var newData = new ByteArray();
            newData.writeByte(TDataType.BYTE_ARRAY);
            newData.writeInt(val.length);
            newData.writeBytes(val, 0, val.length);
            return this.addData(buffer, newData);
        }
        private binEncode_BYTE(buffer: ByteArray, val: number): ByteArray {
            var newData = new ByteArray();
            newData.writeByte(TDataType.BYTE);
            newData.writeByte(val);
            return this.addData(buffer, newData);
        }

        private binEncode_SHORT(buffer: ByteArray, val: number): ByteArray {
            var newData = new ByteArray();
            newData.writeByte(TDataType.SHORT);
            newData.writeShort(val);
            return this.addData(buffer, newData);
        }

        private binEncode_DOUBLE(buffer: ByteArray, val: number): ByteArray {
            var newData = new ByteArray();
            newData.writeByte(TDataType.DOUBLE);
            newData.writeDouble(val);
            return this.addData(buffer, newData);
        }

        private binEncode_FLOAT(buffer: ByteArray, val: number): ByteArray {
            var newData = new ByteArray();
            newData.writeByte(TDataType.FLOAT);
            newData.writeFloat(val);
            return this.addData(buffer, newData);
        }

        private binEncode_INT(buffer: ByteArray, val: number): ByteArray {
            var newData = new ByteArray();
            newData.writeByte(TDataType.INT);
            newData.writeInt(val);
            return this.addData(buffer, newData);
        }

        private binEncode_LONG(buffer: ByteArray, val: number): ByteArray {
            var newData = new ByteArray();
            newData.writeByte(TDataType.LONG);
            newData.writeLong(val);
            return this.addData(buffer, newData);
        }

        private binEncode_NULL(buffer: ByteArray): ByteArray {
            var newData = new ByteArray();
            newData.writeByte(TDataType.NULL);
            return this.addData(buffer, newData);
        }

        private binEncode_STRING(buffer: ByteArray, val: string): ByteArray {
            var newData = new ByteArray();
            newData.writeByte(TDataType.STRING);
            newData.writeString(val);
            return this.addData(buffer, newData);
        }

        private decodeObject(buffer: ByteArray): any {
            var type = buffer.readByte();
            switch (type) {
                case TDataType.NULL:
                    return this.binDecode_NULL(buffer);
                case TDataType.BOOL:
                    return this.binDecode_BOOL(buffer);
                case TDataType.BYTE:
                    return this.binDecode_BYTE(buffer);
                case TDataType.BYTE_ARRAY:
                    return this.binDecode_BYTE_ARRAY(buffer);
                case TDataType.SHORT:
                    return this.binDecode_SHORT(buffer);
                case TDataType.INT:
                    return this.binDecode_INT(buffer);
                case TDataType.LONG:
                    return this.binDecode_LONG(buffer);
                case TDataType.FLOAT:
                    return this.binDecode_FLOAT(buffer);
                case TDataType.DOUBLE:
                    return this.binDecode_DOUBLE(buffer);
                case TDataType.STRING:
                    return this.binDecode_UTF_STRING(buffer);
                case TDataType.TARRAY:
                    buffer.position--;
                    return new TDataWrapper(TDataType.TARRAY, this.decodeTArray(buffer));
            }
            if (type != TDataType.TOBJECT) {
                throw "Unknow DataType ID: " + type;
            }
            buffer.position--;
            var mpObj = this.decodeTObject(buffer);
            return new TDataWrapper(TDataType.TOBJECT, mpObj);
        }

        private decodeTArray(buffer: ByteArray): ITArray {
            var array = TArray.newInstance();
            var type = buffer.readByte();
            if (type != TDataType.TARRAY) {
                throw "Invalid DataType. Expected: " + TDataType.TARRAY + ", found: " + type;
            }
            var num = buffer.readShort();
            if (num < 0) {
                throw "Can't decode Array. Size is negative: " + num;
            }
            for (var i = 0; i < num; i++) {
                var val = this.decodeObject(buffer);
                if (val == null) {
                    throw "Could not decode Array item at index: " + i;
                }
                array.add(val);
            }
            return array;
        }

        private decodeTObject_json(jso:any) : ITObject {
            var object = new TObjectLite();
            for(let key in jso){
                let value = jso[key];
                let decodedObject = this.decodeJsonObject(value);
                if (decodedObject != null)
                    object.put(key, decodedObject);
                else {
                    throw "(json2tobj) Could not decode value for key: " + key;
			    }
            }
            return object;
        }

        private  decodeTArray_json(jsa:any[]):ITArray{
            var array = new TArrayLite();
            for(let value of jsa){
                let decodedObject = this.decodeJsonObject(value);
                if (decodedObject != null)
                    array.add(decodedObject);
                else {
                    throw "(json2tobj) Could not decode value for : " + value;
			    }
            }
            return array;
        }

        private  decodeJsonObject( o:any) :TDataWrapper{
            if ((o.constructor == Number)) {
                return new TDataWrapper(TDataType.DOUBLE, o);
            }
            if ((o.constructor == Boolean)) {
                return new TDataWrapper(TDataType.BOOL, o);
            }
            if ((o.constructor == String)) {
                return new TDataWrapper(TDataType.STRING, o);
            }
            if ((o.constructor == Object)) {
                return new TDataWrapper(TDataType.TOBJECT, this.decodeTObject_json(o));
            }
            if ((o.constructor == Array)) {
                return new TDataWrapper(TDataType.TARRAY, this.decodeTArray_json(o));
            }
    
            throw "Unknown DataType! "+ (o == null ? "null" : o.constructor);
        }

        private decodeTObject(buffer: ByteArray): ITObject {
            var obj2 = TObject.newInstance();
            var num = buffer.readByte();
            if (num != TDataType.TOBJECT) {
                throw "Invalid DataType. Expected: " + TDataType.TOBJECT + ", found: " + num;
            }
            var num2 = buffer.readShort();
            if (num2 < 0) {
                throw "Can't decode Object. Size is negative: " + num2;
            }
            for (var i = 0; i < num2; i++) {
                var key = buffer.readString();
                var val = this.decodeObject(buffer);
                if (val == null) {
                    throw "Could not decode value for Object with key: " + key;
                }
                obj2.put(key, val);
            }
            return obj2;
        }

        public flattenObject(map:any,  obj:ITObject) {
            let keys = obj.getKeys();
            for(let key of keys){
                let value = obj.get(key);
                if (value.type == TDataType.TOBJECT) {
                    let newMap = {};
                    map[key] = newMap;
                    this.flattenObject(newMap, value.data);
                } else if (value.type == TDataType.TARRAY) {
                    let newList = [];
                    map[key] = newList;
                    this.flattenArray(newList, value.data);
                } else {
                    map[key] = value.data;
                }
            }
        }
    
        public flattenArray(array:any[], tarray:ITArray) {
            for(let i =0;i<tarray.size();++i){
                let value = tarray.get(i);
                if (value.type == TDataType.TOBJECT) {
                    let newMap = {};
                    array.push(newMap);
                    this.flattenObject(newMap, value.data);
                } else if (value.type == TDataType.TARRAY) {
                    let newList = [];
                    array.push(newList);
                    this.flattenArray(newList, value.data);
                } else {
                    array.push(value.data);
                }
            }
        }

        private encodeObject(buffer: ByteArray, typeId: number, data: any): ByteArray {
            switch (typeId) {
                case TDataType.NULL:
                    buffer = this.binEncode_NULL(buffer);
                    return buffer;
                case TDataType.BOOL:
                    buffer = this.binEncode_BOOL(buffer, data);
                    return buffer;
                case TDataType.BYTE:
                    buffer = this.binEncode_BYTE(buffer, data);
                    return buffer;
                case TDataType.SHORT:
                    buffer = this.binEncode_SHORT(buffer, data);
                    return buffer;
                case TDataType.INT:
                    buffer = this.binEncode_INT(buffer, data);
                    return buffer;
                case TDataType.LONG:
                    buffer = this.binEncode_LONG(buffer, data);
                    return buffer;
                case TDataType.FLOAT:
                    buffer = this.binEncode_FLOAT(buffer, data);
                    return buffer;
                case TDataType.DOUBLE:
                    buffer = this.binEncode_DOUBLE(buffer, data);
                    return buffer;
                case TDataType.STRING:
                    buffer = this.binEncode_STRING(buffer, data);
                    return buffer;
                case TDataType.TARRAY:
                    buffer = this.addData(buffer, this.array2binary(data));
                    return buffer;
                case TDataType.BYTE_ARRAY:
                    buffer = this.binEncode_BYTE_ARRAY(buffer, data);
                    return buffer;
                case TDataType.TOBJECT:
                    buffer = this.addData(buffer, this.object2binary(data));
                    return buffer;
            }
            throw "Unrecognized type in Object serialization: " + typeId;
        }


        private obj2bin(obj: ITObject, buffer: ByteArray): ByteArray {
            obj.getKeys().forEach(str => {
                var data = obj.get(str);
                buffer.writeString(str);
                buffer = this.encodeObject(buffer, data.type, data.data);
            });

            return buffer;
        }

    }
}